Skip to content

Conversation

@Shubham-275
Copy link

demo.mp4

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

Adds a new Discord bot project "waifu-deal-sniper" including: a production bot implementation (bot.js) that queries the TinyFish MINO SSE API across multiple marketplaces, a SQLite-backed persistence module (database.js) exporting CRUD and analytics functions, message templates (templates.js), package.json, README, .gitignore, and .env.example. Features include multi-site searches, rarity scoring, per-user watchlists with background checks/notifications, gacha/roast/copium modes, rate limiting, and persistent user/watch/notification storage.

Sequence Diagram(s)

sequenceDiagram
    actor User as User/Discord
    participant Bot as Discord Bot
    participant Parser as Intent Parser
    participant MINO as MINO API
    participant DB as SQLite DB

    User->>Bot: DM or mention (search/watch/gacha/roast)
    Bot->>Parser: Parse intent & params
    Parser-->>Bot: Intent & params

    alt Search
        Bot->>MINO: POST search request (SSE-like)
        MINO-->>Bot: Stream JSON events / items
        Bot->>Bot: Parse stream, compute grades & rarity
        Bot->>DB: logSearch / increment stats
        DB-->>Bot: Ack
        Bot-->>User: Send results embed(s)
    end

    alt Add Watch
        Bot->>DB: addToWatchlist / getUserWatchlist
        DB-->>Bot: Ack
        Bot-->>User: Confirm watch added

        par Background Watch Loop
            loop periodic
                Bot->>MINO: Re-run search for active watches
                MINO-->>Bot: Stream current listings
                Bot->>DB: hasBeenNotified / markNotified
                alt New deal found
                    Bot-->>User: Send DM notification
                    DB-->>Bot: Record notification
                end
            end
        end
    end

    alt Gacha / Roast / Copium
        Bot->>DB: getOrCreateUser / getUserStats / get lastSearchResults
        DB-->>Bot: User & cached results
        Bot->>Bot: Apply templates & randomize
        Bot-->>User: Send formatted response
    end
Loading
🚥 Pre-merge checks | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description only contains an external asset link with no textual explanation of the changes, failing to provide meaningful information about the pull request. Add a substantive description explaining the purpose of the changes, key features implemented, and how the new code integrates with the project.
Title check ❓ Inconclusive The title 'Shubham/waifu deal sniper' is vague and uses a developer name prefix that doesn't clearly convey the main change; it lacks specificity about what was added or modified. Revise the title to be more descriptive of the actual changes, such as 'Add Discord bot for anime figure deal scraping via TinyFish API' or 'Implement waifu-deal-sniper bot with multi-site search and watch features'.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@waifu-deal-sniper/bot.js`:
- Around line 1415-1459: The runWatchChecker function can be invoked while a
previous execution is still running, causing overlapping runs and duplicate
notifications; add a reentrancy guard (e.g., a module-level boolean like
isWatchCheckerRunning) or switch to scheduling the next run after completion
(use setTimeout chaining instead of setInterval) so a new invocation returns
immediately if isWatchCheckerRunning is true; set the flag to true at the start
of runWatchChecker and ensure it is cleared in a finally block (or clear after
all watches processed) to avoid leaving it stuck, and update the interval
scheduling code (the setInterval/start call that triggers runWatchChecker) to
use the new non-overlapping pattern.
- Around line 1497-1499: The current console.log call printing message.content
(console.log(`📨 Message from ${message.author.username}:
"${message.content.slice(0, 50)}" (DM: ${isDM})`)) risks leaking PII; change it
so raw DM content is never logged in production by gating detailed logging
behind a DEBUG flag (e.g., process.env.DEBUG_LOGGING === 'true') or by logging
only metadata when the flag is false (author id, username, message length, isDM,
message.id) and, if DEBUG is true, include a safe redacted preview rather than
full content. Locate the console.log in the message handler (references:
message, isDM) and replace/wrap it with a conditional that checks the DEBUG flag
and falls back to metadata-only logging.
- Around line 374-380: The MAX price text in searchSite is hard-coded to "JPY"
causing wrong filtering for other sites; update the construction of the goal
string in async function searchSite (the site.goal + ... line) to use the site's
currency (e.g., site.currency) instead of "JPY", and if the app must compare
across sites either convert maxPrice into the site's currency before appending
or clearly state in the goal which site's currency is expected (e.g., "Only
items under {maxPrice} {site.currency}"). Ensure you reference the maxPrice
handling and site.goal so the message and any filtering logic use the correct
currency semantics.
- Around line 885-894: The main search path never updates lastSearchResults so
subsequent commands like gacha_last or roast can't access the latest results;
after computing result.items (before sending the summary/embed) update the
shared lastSearchResults store for this user (e.g., set
lastSearchResults[user.id] or the appropriate per-user key used by
gacha_last/roast to point to result.items) so the gacha_last and roast handlers
can read the most recent standard search results; place this update near the
block that computes deals (referencing result.items, user.id, and
lastSearchResults/gacha_last/roast).
- Around line 539-590: Sanitize external fields before embedding: in
createFigureEmbed() apply sanitizeForDisplay() to item.name,
item.item_grade/item.condition, item.box_grade (if considered condition), and
any item.manufacturer or item.seller used in the description/title/footer so the
embedded text cannot contain `@everyone/`@here; mirror the same sanitization in
createSiteEmbed() for item.name, item.condition, item.seller and
item.manufacturer referenced there. Also, when sending embeds (the message
send/interaction reply calls that include the EmbedBuilder from
createFigureEmbed/createSiteEmbed), add allowedMentions: { parse: [] } to the
send options to disable mention parsing as defense-in-depth. Ensure you only
transform display strings (don’t mutate raw item objects) and reference the
functions createFigureEmbed and createSiteEmbed when making the changes.

In `@waifu-deal-sniper/package.json`:
- Around line 6-26: The package.json currently sets "engines.node": ">=18.0.0"
while the "dev" script uses "node --watch" which is only stable in Node
>=20.13.0; either raise the engine requirement or remove the dependency on the
experimental flag: update "engines.node" to ">=20.13.0" if you want to keep
using "node --watch", or change the "scripts.dev" entry to use nodemon (e.g.,
"nodemon --watch bot.js bot.js") and add nodemon to devDependencies so
development works on older Node 18.x without forcing a node version bump; modify
the "scripts.dev" and "devDependencies" accordingly and ensure "scripts.start"
and "scripts.dev" remain consistent with the chosen approach.

In `@waifu-deal-sniper/README.md`:
- Line 3: Replace bare URLs in the README with proper Markdown link syntax to
satisfy MD034: change the literal OAuth2 link at the "Live Demo" line into a
labeled link like "Live Demo: [text](https://discord.com/...)" and similarly
convert the other bare URL noted at line 19 into a descriptive label with
bracketed text followed by the URL in parentheses; ensure each link uses the
form [label](URL) and keep descriptive labels (e.g., "Live Demo") for
readability.
- Around line 113-115: The README contains several fenced code blocks missing
language identifiers which fails linting; add a language tag (e.g., "text") to
each triple-backtick fence for the blocks that include the Discord OAuth URL
("https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID..."), the large
ASCII architecture diagram (the block starting with the DISCORD USER/ DISCORD
BOT diagram), and the repository file tree block that begins
"waifu-deal-sniper/"; ensure you update every matching fenced block (also the
other occurrences mentioned around the ASCII/art and file-tree sections) so each
opening ``` becomes ```text.
🧹 Nitpick comments (1)
waifu-deal-sniper/database.js (1)

104-124: Debounce disk writes to avoid blocking on every mutation.
run() triggers saveDb() which exports the full DB and uses a sync write; frequent bot activity could stall the event loop. Consider batching writes or debouncing while keeping the periodic flush.

♻️ Example debounce
+let saveScheduled = false;
+function scheduleSave() {
+  if (saveScheduled) return;
+  saveScheduled = true;
+  setTimeout(() => {
+    saveScheduled = false;
+    saveDb();
+  }, 1000);
+}
+
 function run(sql, params = []) {
   if (!db) throw new Error('Database not initialized');
   db.run(sql, params);
-  saveDb();
+  scheduleSave();
 }

Comment on lines +539 to +590
function createFigureEmbed(item) {
const isGoodDeal = isDeal(item);
const price = parseInt(item.price) || 0;
const rarity = item.rarity?.tier || 'r';
const rarityLabel = item.rarity?.label || '';

// Color based on rarity or deal status
const rarityColors = {
ssr: 0xFFD700, // Gold
sr: 0xA855F7, // Purple
r: 0x3B82F6, // Blue
salt: 0x6B7280, // Gray
};
const embedColor = isGoodDeal ? 0xFF6B6B : (rarityColors[rarity] || 0x6C5CE7);

// Title prefix based on rarity
const rarityPrefix = rarity === 'ssr' ? '🌈 ' : rarity === 'sr' ? '⭐ ' : '';

const embed = new EmbedBuilder()
.setColor(embedColor)
.setTitle(`${isGoodDeal ? '🔥 ' : rarityPrefix}${(item.name || 'Figure').slice(0, 250)}`)
.setURL(item.url || 'https://www.amiami.com');

// Only set thumbnail if it's a valid URL
if (item.image && item.image.startsWith('http')) {
embed.setThumbnail(item.image);
}

let desc = '';
if (isGoodDeal) {
desc += `**${pick(TEMPLATES.deal_alert)}**\n\n`;
} else if (rarity === 'ssr') {
desc += `**${rarityLabel}**\n\n`;
}

desc += `💴 **¥${price.toLocaleString()}**\n`;
desc += `✨ Figure: **${item.item_grade || '?'}** | 📦 Box: **${item.box_grade || '?'}**\n`;
desc += `${item.in_stock !== false ? '✅ In Stock' : '❌ Sold Out'}`;

// Add rarity tags if present
if (item.rarityDetails && item.rarityDetails.length > 0) {
desc += `\n\n🏷️ ${item.rarityDetails.slice(0, 3).join(' • ')}`;
}

desc += `\n\n*${getConditionComment(item.item_grade, item.box_grade)}*`;

embed.setDescription(desc);

// Footer with site info if multi-site
const siteInfo = item.siteEmoji ? `${item.siteEmoji} ${item.siteName} • ` : '';
embed.setFooter({ text: `${siteInfo}${getPriceReaction(price)} • Click title to buy!` });

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "bot.js" -type f | head -20

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 99


🏁 Script executed:

git ls-files | grep -E "bot\.js|\.js$" | head -30

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 158


🏁 Script executed:

wc -l waifu-deal-sniper/bot.js

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 102


🏁 Script executed:

sed -n '539,590p' waifu-deal-sniper/bot.js

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 1896


🏁 Script executed:

sed -n '1045,1085p' waifu-deal-sniper/bot.js

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 1253


🏁 Script executed:

rg -n "sanitizeForDisplay" waifu-deal-sniper/

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 1477


🏁 Script executed:

rg -n "allowedMentions" waifu-deal-sniper/

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 55


🏁 Script executed:

sed -n '65,75p' waifu-deal-sniper/bot.js

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 387


🏁 Script executed:

rg -n "\.send\(|\.reply\(" waifu-deal-sniper/bot.js | head -30

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 2559


🏁 Script executed:

sed -n '895,905p' waifu-deal-sniper/bot.js

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 407


🏁 Script executed:

sed -n '965,975p' waifu-deal-sniper/bot.js

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 401


🏁 Script executed:

sed -n '1033,1040p' waifu-deal-sniper/bot.js

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 425


🏁 Script executed:

rg -n "parse:" waifu-deal-sniper/bot.js

Repository: tinyfish-io/TinyFish-cookbook

Length of output: 55


Sanitize external fields in embeds to prevent @mention abuse.

item.name, item.condition, item.seller, and item.manufacturer come from external sources and are embedded unsanitized—allowing @everyone/@here in listings to ping servers. Apply sanitizeForDisplay() to these fields in both createFigureEmbed() (line 559) and createSiteEmbed() (lines 1059, 1074, 1079, 1083). Additionally, set allowedMentions: { parse: [] } on all embed sends to block mention parsing as defense-in-depth.

🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 539 - 590, Sanitize external fields
before embedding: in createFigureEmbed() apply sanitizeForDisplay() to
item.name, item.item_grade/item.condition, item.box_grade (if considered
condition), and any item.manufacturer or item.seller used in the
description/title/footer so the embedded text cannot contain `@everyone/`@here;
mirror the same sanitization in createSiteEmbed() for item.name, item.condition,
item.seller and item.manufacturer referenced there. Also, when sending embeds
(the message send/interaction reply calls that include the EmbedBuilder from
createFigureEmbed/createSiteEmbed), add allowedMentions: { parse: [] } to the
send options to disable mention parsing as defense-in-depth. Ensure you only
transform display strings (don’t mutate raw item objects) and reference the
functions createFigureEmbed and createSiteEmbed when making the changes.

Comment on lines +885 to +894
// Log & count deals
db.logSearch(user.id, cleanQuery, result.items.length);
const deals = result.items.filter(isDeal);
if (deals.length > 0) {
db.incrementDealsFound(user.id, deals.length);
}

// Send results
const summaryEmbed = createResultsSummaryEmbed(result.items, cleanQuery, spicy);
await statusMsg.edit({ content: searchMsg, embeds: [summaryEmbed] });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Store last results for standard searches.

lastSearchResults is updated for gacha and multi‑site searches but not the main search path, so gacha_last/roast won’t work after normal searches.

🧩 Suggested fix
   // Log & count deals
   db.logSearch(user.id, cleanQuery, result.items.length);
+  lastSearchResults.set(user.discord_id, { query: cleanQuery, items: result.items, timestamp: Date.now() });
   const deals = result.items.filter(isDeal);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Log & count deals
db.logSearch(user.id, cleanQuery, result.items.length);
const deals = result.items.filter(isDeal);
if (deals.length > 0) {
db.incrementDealsFound(user.id, deals.length);
}
// Send results
const summaryEmbed = createResultsSummaryEmbed(result.items, cleanQuery, spicy);
await statusMsg.edit({ content: searchMsg, embeds: [summaryEmbed] });
// Log & count deals
db.logSearch(user.id, cleanQuery, result.items.length);
lastSearchResults.set(user.discord_id, { query: cleanQuery, items: result.items, timestamp: Date.now() });
const deals = result.items.filter(isDeal);
if (deals.length > 0) {
db.incrementDealsFound(user.id, deals.length);
}
// Send results
const summaryEmbed = createResultsSummaryEmbed(result.items, cleanQuery, spicy);
await statusMsg.edit({ content: searchMsg, embeds: [summaryEmbed] });
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 885 - 894, The main search path never
updates lastSearchResults so subsequent commands like gacha_last or roast can't
access the latest results; after computing result.items (before sending the
summary/embed) update the shared lastSearchResults store for this user (e.g.,
set lastSearchResults[user.id] or the appropriate per-user key used by
gacha_last/roast to point to result.items) so the gacha_last and roast handlers
can read the most recent standard search results; place this update near the
block that computes deals (referencing result.items, user.id, and
lastSearchResults/gacha_last/roast).

Comment on lines +1497 to +1499
// Debug logging
console.log(`📨 Message from ${message.author.username}: "${message.content.slice(0, 50)}" (DM: ${isDM})`);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging raw DM content in production.

Current logging includes message content, which can capture PII. Gate this behind a DEBUG flag or log only metadata (length, IDs).

🔒 Suggested change
-    console.log(`📨 Message from ${message.author.username}: "${message.content.slice(0, 50)}" (DM: ${isDM})`);
+    if (process.env.DEBUG_LOGS === 'true') {
+      console.log(`📨 Message from ${message.author.username} (DM: ${isDM}) len=${message.content.length}`);
+    }
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1497 - 1499, The current console.log
call printing message.content (console.log(`📨 Message from
${message.author.username}: "${message.content.slice(0, 50)}" (DM: ${isDM})`))
risks leaking PII; change it so raw DM content is never logged in production by
gating detailed logging behind a DEBUG flag (e.g., process.env.DEBUG_LOGGING ===
'true') or by logging only metadata when the flag is false (author id, username,
message length, isDM, message.id) and, if DEBUG is true, include a safe redacted
preview rather than full content. Locate the console.log in the message handler
(references: message, isDM) and replace/wrap it with a conditional that checks
the DEBUG flag and falls back to metadata-only logging.

Comment on lines +6 to +26
"scripts": {
"start": "node bot.js",
"dev": "node --watch bot.js"
},
"keywords": [
"discord-bot",
"anime",
"figures",
"web-scraping",
"tinyfish",
"mino-api"
],
"author": "Shubham Khandelwal",
"license": "MIT",
"dependencies": {
"discord.js": "^14.14.1",
"sql.js": "^1.10.3"
},
"engines": {
"node": ">=18.0.0"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Node.js --watch flag introduced version stability history

💡 Result:

node --watch flag: introduction + stability timeline

  • Introduced (experimental):

    • v18.11.0 (and backported to v16.19.0) — --watch (and --watch-path) first added; initially Stability: 1 (Experimental). [1][2]
  • Still experimental in early v20:

    • Example: v20.3.1 docs still mark --watch / --watch-path as Experimental. [2]
  • Became stable:

    • v20.13.0 — watch mode marked stable in the Node 20 line. [3]
    • v22.0.0 — watch mode marked stable in the Node 22 line. [1]

Related flags (watch-mode family)

  • --watch-preserve-output added in v19.3.0 and v18.13.0. [4]

Sources: Node.js CLI documentation for various release lines. [1][2][3][4]


Align engines.node with node --watch availability.
npm run dev relies on node --watch, which was introduced in v18.11.0 but remained experimental until v20.13.0. The current requirement >=18.0.0 includes versions that lack this flag entirely (v18.0.0–v18.10.0). Either bump the engine floor to >=20.13.0 for stable watch mode support, or replace node --watch with nodemon to avoid tight version coupling.

♻️ Option A: require stable watch mode
-  "node": ">=18.0.0"
+  "node": ">=20.13.0"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"scripts": {
"start": "node bot.js",
"dev": "node --watch bot.js"
},
"keywords": [
"discord-bot",
"anime",
"figures",
"web-scraping",
"tinyfish",
"mino-api"
],
"author": "Shubham Khandelwal",
"license": "MIT",
"dependencies": {
"discord.js": "^14.14.1",
"sql.js": "^1.10.3"
},
"engines": {
"node": ">=18.0.0"
}
"scripts": {
"start": "node bot.js",
"dev": "node --watch bot.js"
},
"keywords": [
"discord-bot",
"anime",
"figures",
"web-scraping",
"tinyfish",
"mino-api"
],
"author": "Shubham Khandelwal",
"license": "MIT",
"dependencies": {
"discord.js": "^14.14.1",
"sql.js": "^1.10.3"
},
"engines": {
"node": ">=20.13.0"
}
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/package.json` around lines 6 - 26, The package.json
currently sets "engines.node": ">=18.0.0" while the "dev" script uses "node
--watch" which is only stable in Node >=20.13.0; either raise the engine
requirement or remove the dependency on the experimental flag: update
"engines.node" to ">=20.13.0" if you want to keep using "node --watch", or
change the "scripts.dev" entry to use nodemon (e.g., "nodemon --watch bot.js
bot.js") and add nodemon to devDependencies so development works on older Node
18.x without forcing a node version bump; modify the "scripts.dev" and
"devDependencies" accordingly and ensure "scripts.start" and "scripts.dev"
remain consistent with the chosen approach.

@@ -0,0 +1,232 @@
# 🎎 Waifu Deal Sniper

**Live Demo:** [https://discord.com/oauth2/authorize?client_id=1465346765611077871&permissions=277025508352&scope=bot]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Convert bare URLs to proper Markdown links.
This addresses the MD034 lint finding and improves readability.

📝 Suggested edit
-**Live Demo:** [https://discord.com/oauth2/authorize?client_id=1465346765611077871&permissions=277025508352&scope=bot]
+**Live Demo:** [Invite link](https://discord.com/oauth2/authorize?client_id=1465346765611077871&permissions=277025508352&scope=bot)

-https://github.com/user-attachments/assets/demo.mp4
+[Demo video](https://github.com/user-attachments/assets/demo.mp4)

Also applies to: 19-19

🤖 Prompt for AI Agents
In `@waifu-deal-sniper/README.md` at line 3, Replace bare URLs in the README with
proper Markdown link syntax to satisfy MD034: change the literal OAuth2 link at
the "Live Demo" line into a labeled link like "Live Demo:
[text](https://discord.com/...)" and similarly convert the other bare URL noted
at line 19 into a descriptive label with bracketed text followed by the URL in
parentheses; ensure each link uses the form [label](URL) and keep descriptive
labels (e.g., "Live Demo") for readability.

Comment on lines +113 to +115
```
https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=277025508352&scope=bot
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifiers to fenced blocks for lint compliance.

📝 Suggested edit
-```
+```text
 https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&permissions=277025508352&scope=bot

- +text
┌─────────────────────────────────────────────────────────────────────────┐
│ DISCORD USER │
│ │
│ "mercari rem bunny" │
└─────────────────────────────────┬───────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ DISCORD BOT (Node.js) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Message │───▶│ Intent │───▶│ Site Router │ │
│ │ Parser │ │ Router │ │ (amiami/mercari/all) │ │
│ └──────────────┘ └──────────────┘ └───────────┬────────────┘ │
│ │ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────▼────────────┐ │
│ │ SQLite │◀──▶│ Rate │◀──▶│ Search Handler │ │
│ │ Database │ │ Limiter │ │ + Rarity Scoring │ │
│ └──────────────┘ └──────────────┘ └───────────┬────────────┘ │
│ │ │
└───────────────────────────────────────────────────────┼─────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│ TINYFISH MINO API │
│ │
│ POST /v1/automation/run-sse │
│ { url: "https://mercari.com/search?keyword=rem", goal: "..." } │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Headless Browser → Navigate → Extract → Return Structured JSON │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 🇯🇵 │ │ 🇺🇸 │ │ ☀️ │
│ AmiAmi │ │ Mercari │ │ Solaris │
│ (JPY) │ │ (USD) │ │ (USD) │
└──────────┘ └──────────┘ └──────────┘


-```
+```text
waifu-deal-sniper/
├── bot.js          # Main bot logic (1,543 lines)
├── database.js     # SQLite database layer
├── templates.js    # 670+ personality responses
├── package.json    # Dependencies
└── README.md       # This file
</details>




Also applies to: 121-165, 185-192

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

113-113: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @waifu-deal-sniper/README.md around lines 113 - 115, The README contains
several fenced code blocks missing language identifiers which fails linting; add
a language tag (e.g., "text") to each triple-backtick fence for the blocks that
include the Discord OAuth URL
("https://discord.com/oauth2/authorize?client_id=YOUR_CLIENT_ID..."), the large
ASCII architecture diagram (the block starting with the DISCORD USER/ DISCORD
BOT diagram), and the repository file tree block that begins
"waifu-deal-sniper/"; ensure you update every matching fenced block (also the
other occurrences mentioned around the ASCII/art and file-tree sections) so each
opening becomestext.


</details>

<!-- fingerprinting:phantom:medusa:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->

Fix natural language parsing and add USD conversion
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@waifu-deal-sniper/bot.js`:
- Around line 1256-1265: The embed description uses chosen.name unsanitized;
call the existing sanitizeForDisplay function on the name before trimming and
inserting into the EmbedBuilder. Replace the current interpolation
`${(chosen.name || 'Mystery Figure').slice(0, 200)}` with a sanitized value
(e.g., const safeName = sanitizeForDisplay(chosen.name || 'Mystery Figure') and
then use safeName.slice(0,200)) so EmbedBuilder.setDescription uses the
sanitized, truncated string; update the code around EmbedBuilder (the
setDescription line) accordingly.
- Around line 1493-1499: The embed title currently interpolates untrusted
deal.name in embed.setTitle (within the block that calls createFigureEmbed and
discordUser.send), which can contain mentions or markdown—sanitize deal.name
before use; replace (deal.name || watch.query) with a sanitized value using the
existing sanitizeForDisplay utility (or an equivalent mention-escaping function)
and use that sanitized string in embed.setTitle and the discordUser.send content
to prevent accidental mentions/formatting.
🧹 Nitpick comments (1)
waifu-deal-sniper/bot.js (1)

711-711: Hardcoded USD→JPY conversion rate.

The exchange rate of 150 will drift over time. Consider fetching a live rate periodically or making this configurable. For a hobby bot this is acceptable, but worth noting.

Comment on lines +1256 to +1265
const embed = new EmbedBuilder()
.setColor(rarityColors[rarity] || 0x6C5CE7)
.setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`)
.setDescription(`${pick(GACHA_TEMPLATES.reveal)}\n\n**${(chosen.name || 'Mystery Figure').slice(0, 200)}**`)
.addFields(
{ name: '💴 Price', value: `¥${price.toLocaleString()}`, inline: true },
{ name: '✨ Condition', value: `Item: ${chosen.item_grade || '?'} | Box: ${chosen.box_grade || '?'}`, inline: true },
{ name: '📦 Stock', value: chosen.in_stock !== false ? '✅ Available!' : '❌ Sold Out', inline: true }
)
.setURL(chosen.url || 'https://www.amiami.com');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unsanitized item name in gacha embed.

chosen.name is used in embed description without sanitization. Apply sanitizeForDisplay() for consistency with the security model.

🛡️ Suggested fix
   const embed = new EmbedBuilder()
     .setColor(rarityColors[rarity] || 0x6C5CE7)
     .setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`)
-    .setDescription(`${pick(GACHA_TEMPLATES.reveal)}\n\n**${(chosen.name || 'Mystery Figure').slice(0, 200)}**`)
+    .setDescription(`${pick(GACHA_TEMPLATES.reveal)}\n\n**${sanitizeForDisplay(chosen.name || 'Mystery Figure').slice(0, 200)}**`)
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1256 - 1265, The embed description
uses chosen.name unsanitized; call the existing sanitizeForDisplay function on
the name before trimming and inserting into the EmbedBuilder. Replace the
current interpolation `${(chosen.name || 'Mystery Figure').slice(0, 200)}` with
a sanitized value (e.g., const safeName = sanitizeForDisplay(chosen.name ||
'Mystery Figure') and then use safeName.slice(0,200)) so
EmbedBuilder.setDescription uses the sanitized, truncated string; update the
code around EmbedBuilder (the setDescription line) accordingly.

Comment on lines +1493 to +1499
const embed = createFigureEmbed(deal);
embed.setTitle(`🚨 DEAL: ${(deal.name || watch.query).slice(0, 200)}`);

await discordUser.send({
content: `🔔 **Found a deal for "${sanitizeForDisplay(watch.query)}"!**`,
embeds: [embed]
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unsanitized deal.name in watch notification embed.

The embed title uses deal.name which comes from external API and could contain mention strings.

🛡️ Suggested fix
             const embed = createFigureEmbed(deal);
-            embed.setTitle(`🚨 DEAL: ${(deal.name || watch.query).slice(0, 200)}`);
+            embed.setTitle(`🚨 DEAL: ${sanitizeForDisplay(deal.name || watch.query).slice(0, 200)}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const embed = createFigureEmbed(deal);
embed.setTitle(`🚨 DEAL: ${(deal.name || watch.query).slice(0, 200)}`);
await discordUser.send({
content: `🔔 **Found a deal for "${sanitizeForDisplay(watch.query)}"!**`,
embeds: [embed]
});
const embed = createFigureEmbed(deal);
embed.setTitle(`🚨 DEAL: ${sanitizeForDisplay(deal.name || watch.query).slice(0, 200)}`);
await discordUser.send({
content: `🔔 **Found a deal for "${sanitizeForDisplay(watch.query)}"!**`,
embeds: [embed]
});
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1493 - 1499, The embed title currently
interpolates untrusted deal.name in embed.setTitle (within the block that calls
createFigureEmbed and discordUser.send), which can contain mentions or
markdown—sanitize deal.name before use; replace (deal.name || watch.query) with
a sanitized value using the existing sanitizeForDisplay utility (or an
equivalent mention-escaping function) and use that sanitized string in
embed.setTitle and the discordUser.send content to prevent accidental
mentions/formatting.

Added Debug Logging
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@waifu-deal-sniper/bot.js`:
- Around line 1318-1322: In handleGachaLast (same pattern as handleGacha) the
embed uses unsanitized chosen.name; update the EmbedBuilder description to call
sanitizeForDisplay(chosen.name) before using it (and keep the existing fallback
'Mystery Figure' and .slice(0, 200) behavior), so the display string is
sanitized prior to slicing/embedding; locate the EmbedBuilder construction in
handleGachaLast and replace direct uses of chosen.name with
sanitizeForDisplay(chosen.name).
- Around line 1587-1592: The SIGTERM handler in process.on('SIGTERM') currently
calls client.destroy() and exits without persisting the sql.js database; export
the existing saveDb function from the database module (ensure saveDb is added to
module.exports alongside other exports) and update the SIGTERM handler to
require/import that saveDb and call await saveDb() (or call it and wait for its
Promise) before client.destroy() and process.exit(0), handling errors from
saveDb so shutdown still completes.
🧹 Nitpick comments (2)
waifu-deal-sniper/bot.js (2)

940-942: Consider adding allowedMentions: { parse: [] } as defense-in-depth.

Even with sanitization, setting allowedMentions on embed sends provides an extra layer of protection against mention abuse if sanitization is ever bypassed or missed.

🛡️ Example for embed sends
   for (const item of toShow) {
-    await message.channel.send({ embeds: [createFigureEmbed(item)] });
+    await message.channel.send({ 
+      embeds: [createFigureEmbed(item)],
+      allowedMentions: { parse: [] }
+    });
   }

Apply similarly to other message.reply() and message.channel.send() calls that include embeds with external data.


711-711: Hardcoded exchange rate may drift over time.

USD_TO_JPY = 150 is static. Consider fetching from an API or making it configurable via environment variable for accuracy.

Comment on lines +1318 to +1322
const embed = new EmbedBuilder()
.setColor(rarityColors[rarity] || 0x6C5CE7)
.setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`)
.setDescription(`**${(chosen.name || 'Mystery Figure').slice(0, 200)}**\n\n💴 ¥${price.toLocaleString()}`)
.setURL(chosen.url || 'https://www.amiami.com');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

handleGachaLast also uses unsanitized item name.

Same pattern as handleGacha — apply sanitizeForDisplay() to chosen.name.

🛡️ Suggested fix
   const embed = new EmbedBuilder()
     .setColor(rarityColors[rarity] || 0x6C5CE7)
     .setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`)
-    .setDescription(`**${(chosen.name || 'Mystery Figure').slice(0, 200)}**\n\n💴 ¥${price.toLocaleString()}`)
+    .setDescription(`**${sanitizeForDisplay(chosen.name || 'Mystery Figure')}**\n\n💴 ¥${price.toLocaleString()}`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const embed = new EmbedBuilder()
.setColor(rarityColors[rarity] || 0x6C5CE7)
.setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`)
.setDescription(`**${(chosen.name || 'Mystery Figure').slice(0, 200)}**\n\n💴 ¥${price.toLocaleString()}`)
.setURL(chosen.url || 'https://www.amiami.com');
const embed = new EmbedBuilder()
.setColor(rarityColors[rarity] || 0x6C5CE7)
.setTitle(`🎰 ${pick(GACHA_TEMPLATES.rarity[rarity])}`)
.setDescription(`**${sanitizeForDisplay(chosen.name || 'Mystery Figure')}**\n\n💴 ¥${price.toLocaleString()}`)
.setURL(chosen.url || 'https://www.amiami.com');
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1318 - 1322, In handleGachaLast (same
pattern as handleGacha) the embed uses unsanitized chosen.name; update the
EmbedBuilder description to call sanitizeForDisplay(chosen.name) before using it
(and keep the existing fallback 'Mystery Figure' and .slice(0, 200) behavior),
so the display string is sanitized prior to slicing/embedding; locate the
EmbedBuilder construction in handleGachaLast and replace direct uses of
chosen.name with sanitizeForDisplay(chosen.name).

Comment on lines +1587 to +1592
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('👋 Shutting down gracefully...');
client.destroy();
process.exit(0);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's check if database.js exists and its structure
fd -t f "database.js" waifu-deal-sniper/

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Check for close/shutdown/cleanup exports in database module
rg -n "exports\.(close|shutdown|cleanup|quit)" waifu-deal-sniper/ --type js
rg -n "module\.exports.*=" waifu-deal-sniper/database.js

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Check if db is imported or initialized in bot.js
rg -n "require.*database|import.*database|db\s*=" waifu-deal-sniper/bot.js | head -20

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 109


🏁 Script executed:

#!/bin/bash
# Get context around the SIGTERM handler to see if db is available
sed -n '1580,1600p' waifu-deal-sniper/bot.js

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 484


🏁 Script executed:

#!/bin/bash
# Check the full exports of database.js
cat -n waifu-deal-sniper/database.js | tail -100

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 3005


🏁 Script executed:

#!/bin/bash
# Search for method definitions in database.js that might be cleanup-related
rg -n "\.close|\.shutdown|\.cleanup|\.quit" waifu-deal-sniper/database.js

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Get the database module structure - check what properties are exported
sed -n '299,350p' waifu-deal-sniper/database.js

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 440


🏁 Script executed:

#!/bin/bash
# Check the beginning of database.js to see db initialization
head -50 waifu-deal-sniper/database.js

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 1367


🏁 Script executed:

#!/bin/bash
# Search for db variable initialization pattern
rg -n "const db|let db|var db" waifu-deal-sniper/database.js | head -5

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Search for save, export, flush, or persistence methods in database.js
rg -n "export|save|flush|toFile|writeFile|persist" waifu-deal-sniper/database.js

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 294


🏁 Script executed:

#!/bin/bash
# Check if there's any persistence logic - search for fs.writeFile or similar
rg -n "fs\." waifu-deal-sniper/database.js

Repository: tinyfish-io/tinyfish-cookbook

Length of output: 285


Graceful shutdown doesn't save database before exiting.

The database uses sql.js and has an auto-save every 30 seconds via saveDb(), but this isn't called during graceful shutdown. Recent changes made after the last auto-save interval will be lost. The saveDb() function exists but isn't exported or invoked in the SIGTERM handler.

Export saveDb from the database module and call it before shutdown:

🔧 Suggested fix

In database.js, add saveDb to module.exports:

 module.exports = {
   initDb,
   getOrCreateUser,
   // ... other exports
+  saveDb,
   get db() { return db; },
 };

In bot.js, call it during shutdown:

 process.on('SIGTERM', () => {
   console.log('👋 Shutting down gracefully...');
+  db.saveDb();
   client.destroy();
   process.exit(0);
 });
🤖 Prompt for AI Agents
In `@waifu-deal-sniper/bot.js` around lines 1587 - 1592, The SIGTERM handler in
process.on('SIGTERM') currently calls client.destroy() and exits without
persisting the sql.js database; export the existing saveDb function from the
database module (ensure saveDb is added to module.exports alongside other
exports) and update the SIGTERM handler to require/import that saveDb and call
await saveDb() (or call it and wait for its Promise) before client.destroy() and
process.exit(0), handling errors from saveDb so shutdown still completes.

@simantak-dabhade simantak-dabhade removed the request for review from lozzle January 30, 2026 23:22
@gauravreddy08 gauravreddy08 self-requested a review February 2, 2026 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant